//
// Game Animation.
// Copyright (C) 2007 Department of Computer Science, University of Copenhagen
//
#include <animation/configuration.h>

#define DEFINE_GLUT_MAIN
#include <OpenTissue/utility/glut/glut_perspective_view_application.h>
#undef DEFINE_GLUT_MAIN


#include <OpenTissue/utility/gl/gl_util.h>
#include <OpenTissue/gpu/cg/cg_util.h>

#include <animation/game_animation_types.h>
#include <animation/terrain_shader.h>
#include <animation/grass_shader.h>
#include <animation/sky_shader.h>
#include <animation/cell_generator.h>

#include <OpenTissue/core/containers/mesh/trimesh/trimesh.h>
#include <OpenTissue/core/containers/mesh/common/io/mesh_obj_read.h>
#include <OpenTissue/core/containers/mesh/common/util/mesh_compute_angle_weighted_vertex_normals.h>
#include <OpenTissue/core/containers/mesh/common/util/mesh_make_sphere.h>

#include <animation/geometry/mesh2geometry.h>
#include <animation/compute_heightmap_point.h>

#include <iostream>
#include <sstream>

class Application 
  : public OpenTissue::glut::BasePerspectiveViewApplication< OpenTissue::math::BasicMathTypes<float,size_t> >
{
protected:

  typedef OpenTissue::glut::BasePerspectiveViewApplication< OpenTissue::math::BasicMathTypes<float,size_t> > base_class;

  typedef animation::GameAnimationTypes<math_types>   types;

  bool m_action[256];          ///< Boolean array used to keep track of the user actions/selections.

  typedef math_types::real_type                    real_type;
  typedef math_types::vector3_type                 vector3_type;
  typedef math_types::quaternion_type              quaternion_type;
  typedef math_types::matrix3x3_type               matrix3x3_type;

protected:

  bool m_wcs_visible;
  bool m_cam_visible;
  bool m_aabb_visible;
  bool m_depth_visible;
  bool m_terrain_visible;
  bool m_terrain_shader_on;
  bool m_terrain_wireframe;
  bool m_grass_visible;
  bool m_boid_visible;
  bool m_boid_mesh_on;
  bool m_sky_enabled;
  bool m_sky_shader_on;
  bool m_sky_wireframe;
  bool m_follow_boid;  ///< Makes camera follow boid around. Default is off.

protected:

  animation::CellGenerator<types>    m_cell_generator;
  types::grid_manager                m_grid_manager;
  types::cell_sorting                m_cell_sorting;

  unsigned int m_I;            ///< Number of virtual grid cells along x-axis.
  unsigned int m_J;            ///< Number of virtual grid cells along y-axis.
  real_type    m_cell_width;   ///< Size of grid cell along x-axis.
  real_type    m_cell_height;  ///< Size of grid cell along y-axis.

protected:

  animation::GrassShader            m_grass_shader;  
  animation::TerrainShader          m_terrain_shader;

  animation::SkyShader              m_sky_shader;
  types::geometry_type               m_sky_dome;
  real_type                          m_dome_radius;

  real_type                          m_time;

  vector3_type                       m_boid_position;
  vector3_type                       m_boid_normal;
  real_type                          m_boid_steering_angle;
  vector3_type                       m_boid_dof;
  vector3_type                       m_boid_tracking_vector;
  types::geometry_type               m_boid_geometry;

public:

  Application()
  {
    //--- This is how one could change the default size of the main window
    this->width() = 512;
    this->height() = 512;
  }

public:

  char const * do_get_title() const { return "Game Animation Grid Demo Application"; }

  void do_display()
  {
    render_frames( m_wcs_visible, m_cam_visible );
    //--- depth sorting of cells
    m_cell_sorting.sort(this->camera());

    //--- CPU side culling of cells, first cheaper test then more expensive tests
    this->frustum().update();
    types::cell_sorting::cell_iterator cell      = m_cell_sorting.begin();
    types::cell_sorting::cell_iterator cell_end  = m_cell_sorting.end();
    for(;cell!=cell_end;++cell)
    {
      cell->visible() = true;
      if(cell->depth()<0)
      {
        cell->visible() = false;
        continue;
      }
      if(!this->frustum().contains(cell->get_aabb()))
      {
        cell->visible() = false;
        continue;
      }
    }

    render_aabb( m_aabb_visible,m_depth_visible);
    render_terrain(m_terrain_visible, m_terrain_shader_on,m_terrain_wireframe);
    render_grass(m_grass_visible);
    render_boid(m_boid_visible,m_boid_mesh_on);
    render_sky_dome(m_sky_enabled,m_sky_shader_on,m_sky_wireframe);
  }

  void do_action(unsigned char choice)
  {

    real_type const angle = math_types::value_traits::degree()*5.0;
    // Toggle state
    m_action[choice] = ! m_action[choice];
    switch ( choice )
    {
    case 'a':  movement_update(0.0, angle);   break;
    case 's':  movement_update(0.0,-angle);   break;
    case 'w':  movement_update(0.1,0.0);                break;
    case 'z':  movement_update(-0.1,0.0);               break;
    case 'f':
      m_follow_boid = !m_follow_boid;
      std::cout << "follow boid  = " << m_follow_boid << std::endl;
      break;
    case '!':
      m_wcs_visible = !m_wcs_visible;
      std::cout << "wcs frame = " << m_wcs_visible << std::endl;
      break;
    case '\"':
      m_cam_visible = !m_cam_visible;
      std::cout << "camera frame = " << m_cam_visible << std::endl;
      break;
    case '#':
      m_aabb_visible = !m_aabb_visible;
      std::cout << "aabb visible = " << m_aabb_visible << std::endl;
      break;
    case '%':
      m_depth_visible = !m_depth_visible;
      std::cout << "depth info = " << m_depth_visible << std::endl;
      break;
    case '1':
      m_terrain_visible = !m_terrain_visible;
      std::cout << "terrain visible = " << m_terrain_visible << std::endl;
      break;
    case '2':
      m_terrain_shader_on = !m_terrain_shader_on;
      std::cout << "Terrain shader = " << m_terrain_shader_on << std::endl;
      break;
    case '3':
      m_terrain_wireframe = !m_terrain_wireframe;
      std::cout << "Terrain wireframe = " << m_terrain_wireframe << std::endl;
      break;
    case '4':
      m_sky_enabled = !m_sky_enabled;
      std::cout << "sky enabled = " << m_sky_enabled << std::endl;
      break;
    case '5':
      m_sky_shader_on = !m_sky_shader_on;
      std::cout << "sky shader = " << m_sky_shader_on << std::endl;
      break;
    case '6':
      m_sky_wireframe = !m_sky_wireframe;
      std::cout << "sky wireframe = " << m_sky_wireframe << std::endl;
      break;
    case '7':
      m_grass_visible = !m_grass_visible;
      std::cout << "grass visible = " << m_grass_visible << std::endl;
      break;
    case '8':
      m_boid_visible = !m_boid_visible;
      std::cout << "boid visible = " << m_boid_visible << std::endl;
      break;
    case '9':
      m_boid_mesh_on = !m_boid_mesh_on;
      std::cout << "boid mesh = " << m_boid_mesh_on << std::endl;
      break;
    default:    std::cout << "You pressed " << choice << std::endl;    break;
    };
  }

  void do_init_right_click_menu(int main_menu, void menu(int entry))
  {
    int controls = glutCreateMenu( menu );
    glutAddMenuEntry( "move avatar left            [a]", 'a' );
    glutAddMenuEntry( "move avatar right           [s]", 's' );
    glutAddMenuEntry( "move avatar up              [w]", 'w' );
    glutAddMenuEntry( "move avatar right           [z]", 'z' );
    glutAddMenuEntry( "toggle follow boid          [f]", 'f' );
    glutAddMenuEntry( "toggle wcs frame            [!]", '!' );
    glutAddMenuEntry( "toggle camera frame         [\"]", '\"' );
    glutAddMenuEntry( "toggle aabb                 [#]", '#' );
    glutAddMenuEntry( "toggle depth info           [%]", '%' );
    glutAddMenuEntry( "toggle terrain visible      [1]", '1' );
    glutAddMenuEntry( "toggle terrain shader       [2]", '2' );
    glutAddMenuEntry( "toggle terrain wireframe    [3]", '3' );
    glutAddMenuEntry( "toggle sky enabled          [4]", '4' );
    glutAddMenuEntry( "toggle sky shader           [5]", '5' );
    glutAddMenuEntry( "toggle sky wireframe        [6]", '6' );
    glutAddMenuEntry( "toggle grass visible        [7]", '7' );
    glutAddMenuEntry( "toggle boid visible         [8]", '8' );
    glutAddMenuEntry( "toggle boid mesh            [9]", '9' );
    glutSetMenu( main_menu );
    glutAddSubMenu( "Controls", controls );
  }

  void do_init()
  {
    OpenTissue::cg::startup();

    math_types::vector3_type position( 0, -1, 1);
    math_types::vector3_type target(0,0,0);
    math_types::vector3_type up(0,0,1);

    this->camera().init(position, target, up);


    //--- Create a virtual grid
    m_I           = 7;
    m_J           = 7;
    m_cell_width  = 10.0;
    m_cell_height = 10.0;
    m_grid_manager.init(m_I,m_J,m_cell_width,m_cell_height,m_cell_generator);

    //--- Place boid in virtual grid
    m_boid_position = vector3_type(0,0,0);
    m_boid_steering_angle = 0.0;
    m_boid_dof =  vector3_type(0.0,1.0,0.0);
    m_boid_normal =  vector3_type(0.0,0.0,1.0);
    m_boid_tracking_vector = vector3_type(0.75,-2.0,0.75)*3;
    movement_update(0.0,0.0);

    //--- Prepare virtual grid for visualization
    m_cell_sorting.init(m_grid_manager);

    //--- setup sky dome geometry
    OpenTissue::trimesh::TriMesh<> sphere;
    real_type dx = m_I*m_cell_width;
    real_type dy = m_J*m_cell_height;
    m_dome_radius = 1.5*std::min(dx,dy)*.5;
    OpenTissue::mesh::make_sphere(m_dome_radius,32,32,sphere);
    mesh2geometry(sphere,m_sky_dome);

    //--- setup boid geometry
    OpenTissue::trimesh::TriMesh<> boid_mesh;
    OpenTissue::mesh::obj_read(demo_path + "landscape/data/boid.obj",boid_mesh);
    OpenTissue::mesh::compute_angle_weighted_vertex_normals(boid_mesh);
    mesh2geometry(boid_mesh,m_boid_geometry);

    //--- setup UI and time
    m_time                   = 0.0f;
    m_follow_boid            = false;
    m_wcs_visible            = false;
    m_cam_visible            = false;
    m_aabb_visible           = false;
    m_depth_visible          = false;
    m_terrain_visible        = true;
    m_terrain_shader_on      = false;
    m_terrain_wireframe      = false;
    m_grass_visible          = false;
    m_boid_visible           = false;
    m_boid_mesh_on           = true;
    m_sky_enabled            = false;
    m_sky_shader_on          = true;
    m_sky_wireframe          = false;

  }

  void do_run()
  {
    m_time += 0.01f;
  }

  void do_shutdown()
  {
    OpenTissue::cg::shutdown();
  }

 void mouse_up(double cur_x,double cur_y,bool shift,bool ctrl,bool alt,bool left,bool middle,bool right) 
  {
    base_class::mouse_up(cur_x,cur_y,shift,ctrl,alt,left,middle,right) ;
    update_boid_tracking_vector();
  }

  void mouse_move(double cur_x,double cur_y)
  {
    base_class::mouse_move(cur_x,cur_y);
    update_boid_tracking_vector();
  }




public:

  void render_frames(bool wcs,bool camera)
  {
    if(wcs)
      OpenTissue::gl::DrawFrame(vector3_type(),quaternion_type());
    if(camera)
    {
      OpenTissue::gl::ColorPicker(0.1,.1,.1);
      OpenTissue::gl::DrawVector(vector3_type(),m_camera.target());
      OpenTissue::gl::ColorPicker(0,.5,.5);
      OpenTissue::gl::DrawVector(m_camera.target(),m_camera.right());
      OpenTissue::gl::DrawVector(m_camera.target(),m_camera.up());
      OpenTissue::gl::DrawVector(m_camera.target(),m_camera.dof());
    }
  }

  void render_aabb( bool enabled, bool depth_info)
  {
    if(!enabled)
      return;
    types::cell_sorting::cell_iterator cell      = m_cell_sorting.begin();
    types::cell_sorting::cell_iterator cell_end  = m_cell_sorting.end();
    for(;cell!=cell_end;++cell)
    {
      OpenTissue::gl::ColorPicker(1,0,1);
      OpenTissue::gl::DrawAABB( cell->get_aabb(), GL_LINE_LOOP);
      if(depth_info)
      {
        OpenTissue::gl::ColorPicker(1,0,0);
        std::stringstream text;
        text << cell->depth();
        OpenTissue::gl::DrawString(text.str(),cell->get_center()+ vector3_type(0,0,2), m_grid_manager.dx() );
      }
    }
  }

  void render_terrain( bool enabled, bool shader, bool wireframe)
  {
    if(!enabled)
      return;
    if(shader)
      m_terrain_shader.pre_render();
    else
      OpenTissue::gl::ColorPicker(0.7,0.5,.1);
    //--- front-to-back rendering order, needed to do occlusion query and early-z rejection
    types::cell_sorting::cell_reverse_iterator cell      = m_cell_sorting.rbegin();
    types::cell_sorting::cell_reverse_iterator cell_end  = m_cell_sorting.rend();
    if(wireframe)
    {
      for(;cell!=cell_end;++cell)
      {
        if(cell->visible())
          cell->get_geometry(0).brute_opengl_render(GL_LINE_LOOP);
      }
    }
    else
    {
      for(;cell!=cell_end;++cell)
      {
        if(cell->visible())
          cell->get_geometry(0).vertex_object_render();
      }
    }
    if(shader)
      m_terrain_shader.post_render();
  }

  void render_grass( bool enabled)
  {
    if(!enabled)
      return;
    m_grass_shader.pre_render(
      m_camera.right()(0), m_camera.right()(1), m_camera.right()(2),
      m_camera.up()(0)   , m_camera.up()(1)   , m_camera.up()(2),
      m_time,
      100.0f  //--- visibility range of grass clumps, used to be 50
      );

    //--- back to front render order
    types::cell_sorting::cell_iterator cell      = m_cell_sorting.begin();
    types::cell_sorting::cell_iterator cell_end  = m_cell_sorting.end();
    for(;cell!=cell_end;++cell)
    {
      if(cell->visible())
        cell->get_geometry(1).vertex_object_render();
    }
    m_grass_shader.post_render();
  }

  void render_boid(bool enabled,bool use_mesh)
  {
    if(!enabled)
      return;

    if(use_mesh)
    {
      glPushMatrix();
      vector3_type xaxis = m_boid_dof % m_boid_normal;
      vector3_type yaxis = m_boid_normal;
      vector3_type zaxis = -m_boid_dof;
      matrix3x3_type R(
        xaxis(0),yaxis(0),zaxis(0)
        ,xaxis(1),yaxis(1),zaxis(1)
        ,xaxis(2),yaxis(2),zaxis(2)
        );
      OpenTissue::gl::Transform(m_boid_normal); //--- make it hover a little
      OpenTissue::gl::Transform(m_boid_position);
      quaternion_type Q;
      Q = R;
      OpenTissue::gl::Transform(Q);
      OpenTissue::gl::ColorPicker(.75,0.2,0.1);
      m_boid_geometry.vertex_object_render();
      glPopMatrix();
    }
    else
    {
      OpenTissue::gl::ColorPicker(0.7,0.1,.7);
      OpenTissue::gl::DrawPoint(m_boid_position,0.2);
      OpenTissue::gl::ColorPicker(0.1,0.3,.7);
      OpenTissue::gl::DrawVector(m_boid_position,m_boid_normal);
      OpenTissue::gl::ColorPicker(0.1,0.7,.3);
      OpenTissue::gl::DrawVector(m_boid_position, m_boid_dof );
    }

  }

  void render_sky_dome( bool enabled, bool shader, bool wireframe)
  {
    if(!enabled)
      return;
    glPushMatrix();
    OpenTissue::gl::Transform(vector3_type(0,0,-15));
    OpenTissue::gl::Transform(m_boid_position);
    if(shader)
      m_sky_shader.pre_render(-m_dome_radius,-m_dome_radius,-m_dome_radius,2*m_dome_radius,2*m_dome_radius,2*m_dome_radius);
    else
      OpenTissue::gl::ColorPicker(0.1,0.5,.7);
    if(wireframe)
    {
      m_sky_dome.brute_opengl_render(GL_LINE_LOOP);
    }
    else
    {
      m_sky_dome.vertex_object_render();
    }
    if(shader)
      m_sky_shader.post_render();
    glPopMatrix();
  }

  void movement_update(real_type speed, real_type turn)
  {
    m_boid_steering_angle += turn;
    m_boid_dof = OpenTissue::math::Rz(m_boid_steering_angle)*vector3_type(0.0,1.0,0.0);
    m_boid_position += speed*m_boid_dof;
    m_grid_manager.place(m_boid_position,m_cell_generator);
    types::cell_type * cell = &(m_grid_manager.cell(m_grid_manager.s(),m_grid_manager.t()));
    compute_heightmap_point(
      cell->get_geometry(0)
      , m_cell_generator.I()
      , m_cell_generator.J()
      , m_cell_width
      , m_cell_height
      , m_boid_position(0)
      , m_boid_position(1)
      , m_boid_position
      , m_boid_normal
      );
    m_boid_dof = m_boid_normal % unit(m_boid_dof % m_boid_normal);
    if(m_follow_boid)
    {
      vector3_type p = m_boid_position + (OpenTissue::math::Rz(m_boid_steering_angle)*m_boid_tracking_vector);
      vector3_type up =m_boid_normal;
      vector3_type target = m_boid_position;
      this->camera().init(p,target,up);
    }
  }


  void update_boid_tracking_vector()
  {
    if(m_follow_boid)
    {
      m_boid_tracking_vector = OpenTissue::math::Rz(-m_boid_steering_angle) * (m_camera.position() - m_boid_position);
    }
  }

};

OpenTissue::glut::instance_pointer init_glut_application(int /*argc*/, char ** /*argv*/)
{
  OpenTissue::glut::instance_pointer instance;
  instance.reset( new Application() );
  return instance;
}
